﻿/****************************************************************************************************
	Copyright (C) 2010 RapidWebDev Organization (http://rapidwebdev.org)
	Author: Eunge, Legal Name: Jian Liu, Email: eunge.liu@RapidWebDev.org

	The GNU Library General Public License (LGPL) used in RapidWebDev is 
	intended to guarantee your freedom to share and change free software - to 
	make sure the software is free for all its users.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU Library General Public License (LGPL) for more details.

	You should have received a copy of the GNU Library General Public License (LGPL)
	along with this program.  
	If not, see http://www.rapidwebdev.org/Content/ByUniqueKey/OpenSourceLicense
 ****************************************************************************************************/

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Text;
using System.Data.Linq;
using System.Globalization;
using System.Data;
using System.Threading;
using System.Web;
using Common.Logging;

namespace RapidWebDev.Common.Data
{
	/// <summary>
	/// DataContextFactory is used to create linq2sql/EF data context. 
	/// The extreme case why we need it but not directly construct data context by constructor is, 
	/// when we have multiple DataContext defined for different business focus but they're mapping to a same database, 
	/// it's a bit trouble on managing connection string or configuration files generated by linq2sql/EF GUI.
	/// With DataContextFactory, we can easily and better understand to share the connection string between data contexts by the default "connectionString" segment in application configuration file.
	/// And the factory also provides the event for any data contexts created so that we can register callback on post-process before it's returned out.
	/// </summary>
	public static class DataContextFactory
	{
		private static ILog logger = Logger.Instance(typeof(DataContextFactory));
		private static object syncObject = new object();

		static DataContextFactory()
		{
			TransactionScopeContext.AfterTransactionCommitted += new Action<int>(OnTransactionCompleted);
			TransactionScopeContext.AfterTransactionRollback += new Action<int>(OnTransactionCompleted);
		}

		private static void OnTransactionCompleted(int managedThreadId)
		{
			Dictionary<string, DbConnectionReference> dbConnectionReferences = ResolveDbConnectionReferences();
			List<string> connectionStringNames = dbConnectionReferences.Keys.ToList();
			foreach (string connectionStringName in connectionStringNames)
			{
				if (!dbConnectionReferences.ContainsKey(connectionStringName)) continue;

				DbConnectionReference dbConnectionReference = dbConnectionReferences[connectionStringName];
				if (dbConnectionReference.RefCount < 1)
				{
					if (dbConnectionReference.Connection != null
						&& (dbConnectionReference.Connection.State != ConnectionState.Closed || dbConnectionReference.Connection.State != ConnectionState.Connecting))
					{
						dbConnectionReference.Connection.Close();
						DisposeDbConnectionReferences(connectionStringName);
					}
				}
			}
		}

		#region DataContext Configuration Settings

		private static Dictionary<string, DataContextSetting> dataContextSettings;
		private static IDictionary<string, DataContextSetting> DataContextSettings
		{
			get
			{
				if (dataContextSettings == null)
				{
					lock (syncObject)
					{
						if (dataContextSettings == null)
						{
							dataContextSettings = new Dictionary<string, DataContextSetting>();
							DataContextConfiguration dataContextConfiguration = ConfigurationManager.GetSection("dataContext") as DataContextConfiguration;
							foreach (DataContextSetting setting in dataContextConfiguration.DataContextSettings)
							{
								if (!dataContextSettings.ContainsKey(setting.Name))
									dataContextSettings.Add(setting.Name, setting);
							}
						}
					}
				}

				return dataContextSettings;
			}
		}

		#endregion

		/// <summary>
		/// Create a DataContext instance with db connection (the connection state depends on EnabledMSDTC setting in application configuration file) for the generic data context.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <returns></returns>
		public static T Create<T>() where T : DataContext
		{
			string dataContextName = typeof(T).Name;
			if (!DataContextSettings.ContainsKey(dataContextName))
				throw new ConfigurationErrorsException(string.Format(CultureInfo.InvariantCulture, @"The DataContext ""{0}"" is not configured in application configuration file.", dataContextName));

			DataContextSetting dataContextSetting = DataContextSettings[dataContextName];
			string connectionStringName = dataContextSetting.ConnectionStringName;

			DataContextConfiguration dataContextConfiguration = ConfigurationManager.GetSection("dataContext") as DataContextConfiguration;
			DbConnectionReference dbConnectionReference = ResolveDbConnectionReference(connectionStringName);
			T dataContext = DataContextProxyFactory.Create<T>(dbConnectionReference.Connection);
			dataContext.CommandTimeout = dataContextConfiguration.CommandTimeout;
			return dataContext;
		}

		/// <summary>
		/// Dispose dataContext by removing its dbconnection reference in pool.
		/// </summary>
		/// <param name="dataContext"></param>
		public static void Dispose(DataContext dataContext)
		{
			Type dataContextType = dataContext.GetType();
			string dataContextTypeName = dataContextType.Name;
			if (dataContextTypeName.EndsWith("__Proxy", StringComparison.Ordinal))
				dataContextTypeName = dataContextTypeName.Substring(0, dataContextTypeName.LastIndexOf("__Proxy", StringComparison.Ordinal));

			if (!DataContextSettings.ContainsKey(dataContextTypeName)) return;

			DataContextSetting dataContextSetting = DataContextSettings[dataContextTypeName];
			string connectionStringName = dataContextSetting.ConnectionStringName;

			IDictionary<string, DbConnectionReference> dbConnectionReferencesByConnectionStringName = ResolveDbConnectionReferences();
			if (dbConnectionReferencesByConnectionStringName.ContainsKey(connectionStringName))
			{
				DbConnectionReference dbConnectionReference =dbConnectionReferencesByConnectionStringName[connectionStringName];

				// if (dbConnectionReference.RefCount <= 1 && !TransactionScopeContext.HasTransaction)
				// As TransactionScope using System.Transactions.TransactionScope internally, here doesn't require to keep connection if its reference is decreased to zero.
				if (dbConnectionReference.RefCount <= 1)
				{
					if (dbConnectionReference.Connection.State == ConnectionState.Open
						|| dbConnectionReference.Connection.State == ConnectionState.Executing
						|| dbConnectionReference.Connection.State == ConnectionState.Fetching)
					{
						dbConnectionReference.Connection.Close();
						logger.DebugFormat("Connection of thread-{0} is closed forcely.", Thread.CurrentThread.ManagedThreadId);
					}
					else
					{
						logger.DebugFormat("Connection of thread-{0} is closed already.", Thread.CurrentThread.ManagedThreadId);
					}

					logger.DebugFormat("Connection references of thread-{0} are disposed.", Thread.CurrentThread.ManagedThreadId);
					DisposeDbConnectionReferences(connectionStringName);
				}
				else
				{
					logger.DebugFormat("Connection references of thread-{0} are decreased to {1}.", Thread.CurrentThread.ManagedThreadId, dbConnectionReference.RefCount - 1);
					dbConnectionReference.RefCount--;
				}
			}
		}

		#region Resolve dictionary of DbConnectionReference by connection string name for current executing thread.

		private static Dictionary<int, Dictionary<string, DbConnectionReference>> connectionsByConnectionStringNameByThreadId = new Dictionary<int, Dictionary<string, DbConnectionReference>>();

		/// <summary>
		/// Resolve dictionary of DbConnectionReference by connection string name for current executing thread.
		/// </summary>
		/// <returns></returns>
		private static Dictionary<string, DbConnectionReference> ResolveDbConnectionReferences()
		{
			Dictionary<string, DbConnectionReference> connectionsByConnectionStringName;

			if (HttpContext.Current != null) // for web environment.
			{
				const string DbConnectionReferenceKey = "DataContextFactory.DbConnectionReference@Request";
				if (HttpContext.Current.Items.Contains(DbConnectionReferenceKey))
					connectionsByConnectionStringName = HttpContext.Current.Items[DbConnectionReferenceKey] as Dictionary<string, DbConnectionReference>;
				else
				{
					connectionsByConnectionStringName = new Dictionary<string, DbConnectionReference>();
					HttpContext.Current.Items.Add(DbConnectionReferenceKey, connectionsByConnectionStringName);
				}
			}
			else // for desktop environment.
			{
				int currentThreadId = Thread.CurrentThread.ManagedThreadId;
				if (connectionsByConnectionStringNameByThreadId.ContainsKey(currentThreadId))
					connectionsByConnectionStringName = connectionsByConnectionStringNameByThreadId[currentThreadId];
				else
				{
					connectionsByConnectionStringName = new Dictionary<string, DbConnectionReference>();
					connectionsByConnectionStringNameByThreadId.Add(currentThreadId, connectionsByConnectionStringName);
				}
			}

			return connectionsByConnectionStringName;
		}

		private static void DisposeDbConnectionReferences(string connectionStringName)
		{
			Dictionary<string, DbConnectionReference> currentThreadDbConnectionReferences;
			if (HttpContext.Current != null) // for web environment.
			{
				const string DbConnectionReferenceKey = "DataContextFactory.DbConnectionReference@Request";
				if (HttpContext.Current.Items.Contains(DbConnectionReferenceKey))
				{
					currentThreadDbConnectionReferences = HttpContext.Current.Items[DbConnectionReferenceKey] as Dictionary<string, DbConnectionReference>;
					if (currentThreadDbConnectionReferences.ContainsKey(connectionStringName))
						currentThreadDbConnectionReferences.Remove(connectionStringName);

					if (currentThreadDbConnectionReferences.Count == 0)
						HttpContext.Current.Items.Remove(DbConnectionReferenceKey);
				}
			}
			else // for desktop environment.
			{
				int currentThreadId = Thread.CurrentThread.ManagedThreadId;
				if (connectionsByConnectionStringNameByThreadId.ContainsKey(currentThreadId))
				{
					currentThreadDbConnectionReferences = connectionsByConnectionStringNameByThreadId[currentThreadId];
					if (currentThreadDbConnectionReferences.ContainsKey(connectionStringName))
						currentThreadDbConnectionReferences.Remove(connectionStringName);

					if (currentThreadDbConnectionReferences.Count == 0)
						connectionsByConnectionStringNameByThreadId.Remove(currentThreadId);
				}
			}
		}

		#endregion

		internal static DbConnectionReference ResolveDbConnectionReference(string connectionStringName)
		{
			IDictionary<string, DbConnectionReference> dbConnectionReferencesByConnectionStringName = ResolveDbConnectionReferences();

			DbConnectionReference dbConnectionReference = null;
			// if there has a connection pooled for the connectionStringName. 
			if (dbConnectionReferencesByConnectionStringName.ContainsKey(connectionStringName))
			{
				dbConnectionReference = dbConnectionReferencesByConnectionStringName[connectionStringName];
				if (dbConnectionReference.Connection.State == ConnectionState.Closed)
				{
					logger.DebugFormat("Pooled database connection of thread-{0} is closed. The connection reference is set to 1.", Thread.CurrentThread.ManagedThreadId);
					dbConnectionReference.RefCount = 1;
					TryToOpenDbConnection(dbConnectionReference.Connection);
				}
				else
				{
					dbConnectionReference.RefCount++;
					logger.DebugFormat("Connection references of thread-{0} are increased to {1}.", Thread.CurrentThread.ManagedThreadId, dbConnectionReference.RefCount);
				}

				return dbConnectionReference;
			}
			else
			{
				DataContextSetting dataContextSetting = DataContextSettings.Values.FirstOrDefault(setting => string.Equals(setting.ConnectionStringName, connectionStringName, StringComparison.OrdinalIgnoreCase));
				if (dataContextSetting == null)
					throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, @"The connection string name ""{0}"" is not configured in application configuration file.", connectionStringName), "connectionStringName");

				Type connectionType = Kit.GetType(dataContextSetting.ConnectionType);
				string connectionString = ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString;
				IDbConnection connection = Activator.CreateInstance(connectionType, connectionString) as IDbConnection;
				TryToOpenDbConnection(connection);
				dbConnectionReference = new DbConnectionReference(connection);
				dbConnectionReferencesByConnectionStringName.Add(connectionStringName, dbConnectionReference);
				return dbConnectionReference;
			}
		}

		internal static string ResolveConnectionStringName<TDataContext>() where TDataContext : DataContext
		{
			Type dataContextType = typeof(TDataContext);
			string dataContextTypeName = dataContextType.Name;
			if (dataContextTypeName.EndsWith("__Proxy", StringComparison.Ordinal))
				dataContextTypeName = dataContextTypeName.Substring(0, dataContextTypeName.LastIndexOf("__Proxy", StringComparison.Ordinal));
			if (!DataContextSettings.ContainsKey(dataContextTypeName)) 
				throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, @"The data context name ""{0}"" is not configured in application configuration files.", dataContextTypeName), "TDataContext");

			DataContextSetting dataContextSetting = DataContextSettings[dataContextTypeName];
			return dataContextSetting.ConnectionStringName;
		}

		internal static void ValidateConnectionStringName(string connectionStringName)
		{
			if (ConfigurationManager.ConnectionStrings[connectionStringName] == null)
				throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, @"The connection string name ""{0}"" is not configured in application configuration files.", connectionStringName), "connectionStringName");
		}

		internal static bool DoesCurrentThreadWithinTransaction(string connectionStringName)
		{
			Dictionary<string, DbConnectionReference> dbConnectionReferenceForCurrentThread = ResolveDbConnectionReferences();
			if (!dbConnectionReferenceForCurrentThread.ContainsKey(connectionStringName)) return false;

			DbConnectionReference dbConnectionReference = dbConnectionReferenceForCurrentThread[connectionStringName];
			if (dbConnectionReference == null) return false;
			if (dbConnectionReference.Transaction == null) return false;
			if (dbConnectionReference.Transaction.Transaction == null) return false;

			return dbConnectionReference.Transaction.RefCount > 0;
		}

		private static void TryToOpenDbConnection(IDbConnection connection)
		{
			// As TransactionScope using System.Transactions.TransactionScope internally, here is not required to control the connection state manually.
			//if (connection.State == ConnectionState.Closed)
			//{
			//    DataContextConfiguration dataContextConfiguration = ConfigurationManager.GetSection("dataContext") as DataContextConfiguration;
			//    if (!dataContextConfiguration.EnabledMSDTC) connection.Open();
			//}
		}
	}

	internal class DbConnectionReference
	{
		/// <summary />
		public int RefCount { get; set; }

		/// <summary />
		public IDbConnection Connection { get; private set; }

		/// <summary />
		public DbTransactionReference Transaction { get; set; }

		/// <summary />
		public DbConnectionReference(IDbConnection connection)
		{
			this.Connection = connection;
			this.RefCount = 1;
		}
	}

	internal class DbTransactionReference
	{
		/// <summary />
		public int RefCount { get; set; }

		/// <summary />
		public IDbTransaction Transaction { get; private set; }

		/// <summary />
		public DbTransactionReference(IDbTransaction transaction)
		{
			this.Transaction = transaction;
			this.RefCount = 1;
		}
	}
}